#  injector.rb
#
#  This file is part of myvnet.
#
#  Copyright (C) 2008  Filipe David Borba Manana,  fdmanana@gmail.com
#
#  myvnet is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  myvnet is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with myvnet.  If not, see <http://www.gnu.org/licenses/>.
 

require 'socket'
require 'thread'
require 'yaml'
require 'lib/utils'
require 'lib/protocols/ethernet'
require 'lib/exceptions'


module Packet
  
  # Injects packets into the network.
  # 
  # Client code invokes the instance method add_packet, which
  # adds a packet to an internal queue that is accessed in a
  # FCFS (First Come First Served) fashion. Worker threads of
  # this class will then retrieve the packets from the queue
  # and inject them into the network.
  #
  class Injector
    
    # === Synopsis
    #
    #   Packet::Injector.new(interface)
    #
    # === Args
    #
    # +interface+::
    #   Name of the network interface to use for packet injection.
    #
    # === Exceptions
    #
    # +RawSocketError+::
    #   If by some reason it was not possible to create a Linux Raw Socket
    #   for packet injection.
    #
    # === Description
    #
    # Instantiates a packet injector.
    #    
    def initialize(interface)
      @iface = interface
      @socket = Packet::Injector::RawSocket.new @iface
      @packet_queue = []
      @queue_mutex = Mutex.new
      @empty_queue = ConditionVariable.new
      @worker_threads = ThreadGroup.new
      
      # Create worker threads.
      # They fetch packets from the queue and inject them
      # into the network.
      #
      1.upto(3) do |i|
        
        t = Thread.new(i) { |id|
          Thread.current[:id] = id
          
          while true do
            packet = get_packet
            inject_packet(packet)            
          end
        }
        
        @worker_threads.add t
      end
      
    end
    
    
    # === Synopsis
    # 
    #   Packet::Injector#add_packet(packet, daemon_name)
    #   
    # === Args
    # 
    # +packet+::
    #   A layer 2 packet, of the TCP/IP protocol suite, to inject into
    #   the network. At the moment only Ethernet is supported as a
    #   layer 2 protocol. By other words, it must be an instance of
    #   Ethernet::Packet.
    #   
    # +daemon_name+::
    #   Name of the daemon who invoked this method. Used for debugging
    #   and logging.
    # 
    # === Return
    # 
    # No value.
    # 
    # === Description
    # 
    # Adds a packet, built by a daemon, into the outgoing packets queue.
    # The packet will then be injected to the network as soon as possible.
    #
    def add_packet(packet, daemon_name)
      @queue_mutex.synchronize do
        @packet_queue.push packet                
        @empty_queue.signal                
      end
      
      if $log[:packet].debug?
        msg = "Packet added to outgoing queue "
        msg << "by daemon `#{daemon_name}':\n"
        msg << packet.to_yaml
        $log[:packet].debug msg
      end      
    end
    
    
    def shutdown
      @socket.close
    end
    
    
    
    class RawSocket
    
      PF_PACKET    = 17        # linux/socket.h
      AF_PACKET    = PF_PACKET # linux/socket.h
      ETH_P_ALL    = 0x00_03   # linux/if_ether.h
      SIOCGIFINDEX = 0x89_33   # bits/ioctls.h
    
    
      attr_reader :interface
    
      def initialize(interface)
        @interface = interface
      
        @sock = nil
        begin
          # check Linux man page 7 for "packet"
          @sock = Socket.new(PF_PACKET, Socket::SOCK_RAW,
                             Utils.htons(ETH_P_ALL))
        
          # struct ifreq in net/if.h        
          ifreq = [@interface].pack 'a16'
          ifreq << ( [""].pack 'a16' )
          @sock.ioctl(SIOCGIFINDEX, ifreq)
        
          # struct sockaddr_ll in linux/if_packet.h
          sll = [AF_PACKET].pack 'S'                    # sll_family
          sll << ( [ETH_P_ALL].pack 'S' )               # sll_protocol
          sll << ifreq[16 .. (16 + Utils::SIZEOF_INT)]  # sll_ifindex
          sll << ( [0].pack 'S' )                       # sll_hatype          
          sll << ( [0].pack 'C' )                       # sll_pkttype
          sll << ( [0].pack 'C' )                       # sll_halen
          sll << ( ([0].pack 'C') * 8 )                 # sll_addr[8]
          @sock.bind sll
        
        rescue Exception => ex
          @sock.close if @sock        
          raise RawSocketError, ex.message
        end
      end
    
    
      def send(packet)
        bytes = packet.to_bstream
      
        cnt = -1
        begin
          cnt = @sock.write bytes
          @sock.flush
        rescue Exception => ex
          $log[:packet].error "RawSocket send failed: #{ex.message}\n"
        end
      
        sent = false
        if cnt == -1
          $log[:packet].error "RawSocket send returned -1.\n"        
        elsif cnt < bytes.length
          msg << "RawSocket sent only #{cnt} bytes out of #{bytes.length}.\n"
          $log[:packet].error msg
        else
          sent = true
        end
      
        sent
      end
      
      
      def close
        @sock.close
      end
    
    end

    
    private
    
    def get_packet
      packet = nil
      
      @queue_mutex.synchronize do
        @empty_queue.wait(@queue_mutex) while @packet_queue.empty?
        packet = @packet_queue.shift        
      end
      
      if $log[:packet].debug?
        thread_id = Thread.current[:id]
        msg = "Packet taken from outgoing queue"
        msg << " by thread `#{thread_id}':\n"
        msg << packet.to_yaml
        $log[:packet].debug msg
      end      
            
      packet      
    end
    
            
    def inject_packet(packet)
      thread_id = Thread.current[:id]
      
      unless packet.instance_of? Ethernet::Packet
        msg = "Thread `#{thread_id}', invalid packet type to inject:"
        msg << " `#{packet.class.name}'"
        $log[:packet].error msg
        return
      end
      
      sent = @socket.send packet
      
      if sent
        msg = "Thread `#{thread_id}' injected packet:\n"
        msg << packet.to_yaml
        $log[:packet].info msg
      end
    end    
    
  end  
  
end
